﻿//-----------------------------------------------------------------------
// <copyright file="ConsoleViewModel.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>15/07/2012</date>
//-----------------------------------------------------------------------

using ConsoleHoster.Common.Model;
using ConsoleHoster.Common.UserInput;
using ConsoleHoster.Common.Utilities;
using ConsoleHoster.Common.Utilities.IO;
using ConsoleHoster.Common.Utilities.Threading;
using ConsoleHoster.Common.ViewModel;
using ConsoleHoster.Model;
using ConsoleHoster.Model.Configuration;
using ConsoleHoster.Model.Suggestions;
using ConsoleHoster.ViewModel.ControlVM;
using ConsoleHoster.ViewModel.Entities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;

namespace ConsoleHoster.ViewModel
{
    public partial class ConsoleViewModel : ViewModelBase
    {
        public const string DEFAULT_COMMAND = "cmd.exe";

        private const string MESSAGES_PROPERTY_NAME = "Messages";
        private const double DEFAULT_FONT_SIZE = 12;

        public event EventHandler CommandExecuted;
        public event EventHandler CommandRetrievedFromHistory;

        #region Fields
        private ObservableCollection<OutputMessage> history = new ObservableCollection<OutputMessage>();
        private ObservableQueue<OutputMessage> recentHistory;
        private string currentCommand;
        private ProcessWrapper underlyingProcess;
        private bool isAlive = false;
        private ConsoleProjectViewModel project = null;
        private int commandHistoryIndex = 0;
        private IList<string> commandHistory = new List<string>();
        private SearchViewModel searchVM;
        private IExplorerViewModel explorerVM;

        private readonly Lazy<ICommand> closeCommand;
        private readonly Lazy<ICommand> projectCommand;
        private readonly Lazy<ICommand> retrieveRecentCommand;
        private readonly Lazy<ICommand> clearHistoryCommand;
        private readonly Lazy<ICommand> breakExecutionCommand;
        private readonly Lazy<ICommand> runTypedCommandCommand;

        private readonly Object historySyncObject = new Object();
        private bool showRecentMessagesOnly = false;
        private bool autoScroll = true;
        private bool autoSyncWithCurrentFolder;
        private string currentFolder = null;
        private bool showExplorerPane = true;
        private bool showCommandsPane = true;
        private bool isReady = false;

        private int zoomLevel = 100;

        private readonly IPathSuggestionContext pathSuggestionContext;
        private bool suggestionProvided = false;

        private GridLength explorerViewWidth = GridLength.Auto;
        private readonly IKeyboardStateProvider keyboardStateProvider;
        private ProcessPerfCounterViewModel cpuUsageVM;

        private readonly IKeyboardInputHandler keyboardInputHandler;
        private readonly ILogger outputLogger;
        private readonly string outputLogPath;
        #endregion

        public ConsoleViewModel(
            ConsoleProjectViewModel argProject,
            IEnumerable<CommandDataViewModel> globalCommands,
            IDispatcher argDispatcher,
            IConfigurationProvider argConfigurationProvider,
            IKeyboardStateProvider argKeyboardStateProvider,
            FontSettings argFontSettings,
            string argInstanceName,
            ILogger argLogger,
            ILogger argOutputLogger,
            string argOutputLogPath)
            : this(
                argProject,
                globalCommands,
                argDispatcher,
                argConfigurationProvider,
                new PathSuggestionContext(new PathSuggestionProvider()),
                argKeyboardStateProvider,
                new KeyboardInputHandler(argKeyboardStateProvider, argLogger),
                argFontSettings,
                argInstanceName,
                argLogger,
                argOutputLogger,
                argOutputLogPath)
        {
        }

        internal ConsoleViewModel(
            ConsoleProjectViewModel argProject,
            IEnumerable<CommandDataViewModel> globalCommands,
            IDispatcher argDispatcher,
            IConfigurationProvider argConfigurationProvider,
            IPathSuggestionContext argPathSuggestionContext,
            IKeyboardStateProvider argKeyboardStateProvider,
            IKeyboardInputHandler argKeyboardInputHandler,
            FontSettings argFontSettings,
            string argInstanceName,
            ILogger argLogger,
            ILogger argOutputLogger,
            string argOutputLogPath)
            : base(argDispatcher, new ComponentLogger("ConsoleViewModel('" + argInstanceName + "')", argLogger))
        {
            ValidationManager.RequireArgumentNotNull(argProject, "argProject");
            ValidationManager.RequireArgumentNotNull(argKeyboardStateProvider, "argKeyboardStateProvider");
            ValidationManager.RequireArgumentNotNull(argPathSuggestionContext, "argPathSuggestionContext");
            ValidationManager.RequireArgumentNotNull(argConfigurationProvider, "argConfigurationProvider");

            this.Name = argInstanceName;
            this.IsBusy = false;

            this.outputLogger = argOutputLogger ?? EmptyLogger.Instance;
            this.outputLogPath = argOutputLogPath;

            this.commandManager = new CommandHandlingManager(this, this.Logger);
            this.CommandManager.RegisterPreProcessCommandHandler("cls", cmd => this.ClearHistory());

            this.recentHistory = new ObservableQueue<OutputMessage>(argConfigurationProvider.NumberOfLinesToShowInShowRecentMessagesOnlyMode);

            this.CurrentFontSettings = argFontSettings;
            this.pathSuggestionContext = argPathSuggestionContext;
            this.keyboardStateProvider = argKeyboardStateProvider;
            this.keyboardInputHandler = argKeyboardInputHandler;
            this.Project = argProject;
            this.globalCommands = globalCommands;

            this.searchVM = new SearchViewModel(this.Dispatcher, this.Logger);

            this.explorerVM = new ExplorerViewModel(this, this.keyboardStateProvider, new FileSystemContentProvider(), this.Dispatcher, this.Logger);

            //This is critical to have the AutoSyncWithCurrentFolder to be set after ExplorerVM initalization
            this.AutoSyncWithCurrentFolder = true;

            this.ShowExplorerPane = argProject.ShowExplorerTreeByDefault;

            this.closeCommand = new Lazy<ICommand>(() => new RelayCommand(o => this.Close()));
            this.projectCommand = new Lazy<ICommand>(() => new RelayCommand(tmpCommand => this.ExecuteCommand(tmpCommand as CommandDataViewModel)));
            this.retrieveRecentCommand = new Lazy<ICommand>(() => new RelayCommand(retrieveNext => this.LoadCommandFromHistory((bool)retrieveNext)));
            this.breakExecutionCommand = new Lazy<ICommand>(() => new RelayCommand(o => this.BreakExecution()));
            this.clearHistoryCommand = new Lazy<ICommand>(() => new RelayCommand(o => this.ClearHistory()));
            this.runTypedCommandCommand = new Lazy<ICommand>(() => new RelayCommand(o =>
            {
                string tmpCommandToRun = this.CurrentCommand ?? String.Empty;
                this.CurrentCommand = String.Empty;
                this.RunCommand(tmpCommandToRun);
            }));
        }

        public async Task StartAsync()
        {
            await this.RunAsyncAction(() =>
                {
                    Task tmpDrivesRetrivalTask = this.ExplorerVM.RetreieveDrives();
                    if (!this.IsInDesignMode)
                    {
                        if (this.ShowExplorerPane)
                        {
                            this.ExplorerVM.NavigateTo(this.Project.WorkingDir);
                        }

                        this.StartBackgroundConsole();
                    }

                    tmpDrivesRetrivalTask.Wait();
                }, true, "Starting project...", true);
        }

        public void ExecuteCommand(CommandDataViewModel argCommandData)
        {
            if (argCommandData == null)
            {
                this.ErrorMessage = "No command to execute";
                return;
            }

            if (argCommandData.IsFinal && !Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
            {
                this.RunCommand(argCommandData.CommandText);
            }
            else
            {
                this.CurrentCommand = argCommandData.CommandText;
            }

            this.OnCommandExecuted();
        }

        public async void ClearHistory()
        {
            this.Logger.LogMessage("ClearHistory: Starting");
            await this.RunActionOnUIThread(() =>
            {
                new Action(() =>
                {
                    this.RunActionWithErrorHandling(() =>
                    {
                        this.History.Clear();
                        this.RecentHistory.Clear();
                        if (this.IsReady)
                        {
                            this.RunCommand(String.Empty);
                        }
                        this.OnCommandExecuted();
                    });
                }).SyncronizeCallByLocking(this.historySyncObject, "HistorySyncObject", 1000, true);
            });
            this.Logger.LogMessage("ClearHistory: Ending");
        }

        public void BreakExecution()
        {
            if (!this.IsReady)
            {
                this.RunActionWithErrorHandling(() => this.underlyingProcess.TryBreak(), "Failed to break the process");
            }
        }

        public bool Zoom(int argDelta)
        {
            bool tmpResult = false;

            this.RunActionWithErrorHandling(() =>
                {
                    if (this.keyboardStateProvider.IsKeyDown(Key.LeftCtrl))
                    {
                        this.ZoomLevel += argDelta > 0 ? 5 : -5;
                        tmpResult = true;
                    }
                });

            return tmpResult;
        }

        private void StartBackgroundConsole()
        {
            this.Logger.LogMessage("Starting the background process");

            ProcessStartInfo tmpInfo = new ProcessStartInfo();
            if (this.Project != null)
            {
                tmpInfo.Arguments = this.Project.Arguments;
                tmpInfo.FileName = this.Project.Executable;
                tmpInfo.WorkingDirectory = this.Project.WorkingDir;
            }
            else
            {
                tmpInfo.FileName = DEFAULT_COMMAND;
            }

            if (String.IsNullOrWhiteSpace(tmpInfo.FileName))
            {
                tmpInfo.FileName = DEFAULT_COMMAND;
            }
            tmpInfo.RedirectStandardError = true;
            tmpInfo.RedirectStandardInput = true;
            tmpInfo.RedirectStandardOutput = true;
            tmpInfo.UseShellExecute = false;
            tmpInfo.CreateNoWindow = true;

            this.underlyingProcess = new ProcessWrapper(tmpInfo, this.Logger);

            try
            {
                this.underlyingProcess.DataReceived += OnProcessDataReceived;
                this.underlyingProcess.ProcessExited += OnCommandProcessExited;
                this.underlyingProcess.StartProcess();
                this.OnStarted();
                this.IsAlive = true;
            }
            catch (Exception ex)
            {
                this.Logger.LogError("Error while starting background process for. Details: {0}", ex.ToString());

                if (this.underlyingProcess != null)
                {
                    this.underlyingProcess.Dispose();
                }

                this.IsAlive = false;
                this.ErrorMessage = "Unable to start the process: " + ex.Message;
            }
        }

        private void OnCommandProcessExited(ProcessWrapper sender)
        {
            this.underlyingProcess.Dispose();
            this.CpuUsageVM.StopProbing();
            this.IsReady = true;
            this.IsAlive = false;
            this.Logger.LogMessage("Exited");
        }

        private void OnProcessDataReceived(ProcessWrapper sender, ProcessMessage argMessage)
        {
            this.outputLogger.LogMessage("Message type: '{0}'\tMessage: '{1}'", argMessage.IsOutputMessage ? "Data" : "Error", argMessage.Message.Message.Trim());
            if (argMessage.IsOutputMessage)
            {
                this.AddMessageToHistory(argMessage.Message.Message, this.Project.MessageColor);
                this.CheckFolderChanged(argMessage.Message.Message);
            }
            else
            {
                this.AddMessageToHistory(argMessage.Message.Message, this.Project.ErrorMessageColor);
            }
        }

        private void OnStarted()
        {
            this.Logger.LogMessage("Console started successfully.");

            this.CpuUsageVM = new ProcessPerfCounterViewModel(this.underlyingProcess.Name, this.Dispatcher, this.Logger);
            this.CpuUsageVM.ProbeInterval = TimeSpan.FromSeconds(1);
            this.CpuUsageVM.StartProbing();
        }

        private void OnCurrentCommandChanged(string argOldValue)
        {
            const string tmpSuggestionRequestChar = "\t";

            string tmpOldCommand = argOldValue ?? String.Empty;
            string tmpNewCommand = this.CurrentCommand ?? String.Empty;

            if (tmpNewCommand.Contains(tmpSuggestionRequestChar) && tmpNewCommand.Replace(tmpSuggestionRequestChar, String.Empty) == tmpOldCommand)
            {
                // A tab symbol was just added, which means - a suggestion has been requested.
                this.RunActionWithErrorHandling(() =>
                {
                    this.TryProvideSuggestion();
                }, String.Format("Failed to provide suggestion for command: '{0}'.", this.CurrentCommand));

                this.RemoveTabsFromCurrentCommandSilently();
            }
            else
            {
                this.PathSuggestionContext.Reset();
            }
        }

        private void TryProvideSuggestion()
        {
            if (this.CurrentFolder != null && this.PathSuggestionContext.UpdateContext(this.CurrentCommand, this.CurrentFolder))
            {
                string tmpLastSuggestion = this.PathSuggestionContext.LastProvidedSuggestion;

                string tmpSuggestion = this.keyboardStateProvider.AnyOfKeysDown(Key.LeftShift, Key.RightShift) ?
                    this.PathSuggestionContext.GetPreviousSuggestion() : this.PathSuggestionContext.GetNextSuggestion();
                if (tmpSuggestion != null)
                {
                    this.SuggestionProvided = true;
                    string tmpTextToReplace = tmpLastSuggestion + "\t";

                    this.currentCommand = this.CurrentCommand.Replace(tmpTextToReplace, tmpSuggestion);
                }
            }
        }

        private void RemoveTabsFromCurrentCommandSilently()
        {
            this.currentCommand = this.CurrentCommand.Replace("\t", String.Empty);
        }

        private async void AddMessageToHistory(string argMessage, Color argColor)
        {
            OutputMessage message = new OutputMessage(argMessage, argColor);
            await this.RunActionOnUIThread(
                () =>
                {
                    new Action(() =>
                        {
                            bool tmpShouldAddNew = true;
                            if ((argMessage.Trim() == argMessage || String.IsNullOrWhiteSpace(argMessage)) && this.History.Any())
                            {
                                OutputMessage tmpPreviousMessage = this.History.Last();
                                if (!tmpPreviousMessage.Message.EndsWith(Environment.NewLine))
                                {
                                    tmpPreviousMessage.Message += argMessage;
                                    tmpShouldAddNew = false;
                                }
                            }

                            if (tmpShouldAddNew)
                            {
                                this.History.Add(message);
                                this.RecentHistory.Add(message);
                            }
                        }).SyncronizeCallByLocking(this.historySyncObject, "HistorySyncObject");
                }, false, DispatcherPriority.Background);
        }

        private async void CheckFolderChanged(string argLastMessage)
        {
            string tmpPossiblePath = argLastMessage.Trim();
            bool tmpIsReady = Regex.IsMatch(tmpPossiblePath, @"^[a-zA-Z]:(\\|(\\[\S ]+)*)>$");
            if (tmpIsReady)
            {
                string tmpCurrentFolder = tmpPossiblePath.Trim('>');
                if (tmpCurrentFolder != this.CurrentFolder)
                {
                    this.CurrentFolder = tmpCurrentFolder;
                }
            }

            await this.RunActionOnUIThread(() =>
                {
                    this.IsReady = tmpIsReady;
                }, false, DispatcherPriority.Normal);
        }

        private void OnCurrentFolderChanged()
        {
            this.Logger.LogMessage("Current folder chagned to {0}", this.CurrentFolder);
            if ((this.AutoSyncWithCurrentFolder || !this.IsReady) && this.ShowExplorerPane)
            {
                try
                {
                    this.ExplorerVM.NavigateTo(this.CurrentFolder);
                }
                catch (Exception ex)
                {
                    this.Logger.LogError("Explorer navigation to folder '{0}' failed. Details: {1}", this.Name, this.CurrentFolder, ex.ToString());
                }
            }
        }

        private void OnCommandExecuted()
        {
            EventHandler temp = this.CommandExecuted;
            if (temp != null)
            {
                temp(this, EventArgs.Empty);
            }
        }

        private void OnCommandRetrievedFromHistory()
        {
            EventHandler temp = this.CommandRetrievedFromHistory;
            if (temp != null)
            {
                temp(this, EventArgs.Empty);
            }
        }

        private void LoadCommandFromHistory(bool argRetrieveNext)
        {
            this.RunActionWithErrorHandling(() =>
                {
                    if (argRetrieveNext)
                    {
                        this.CommandHistoryIndex++;
                    }
                    else
                    {
                        this.CommandHistoryIndex--;
                    }

                    if (this.CommandHistory.Any())
                    {
                        this.CurrentCommand = this.CommandHistory[this.CommandHistoryIndex];
                        this.OnCommandRetrievedFromHistory();
                    }
                });
        }

        private void NotifyWaitIndicatorVisibilityChange()
        {
            this.NotifyPropertyChange("WaitIndicatorVisibility");
        }

        #region Properties
        public ObservableCollection<OutputMessage> Messages
        {
            get
            {
                return this.ShowRecentMessagesOnly ? this.RecentHistory : this.History;
            }
        }

        public ObservableCollection<OutputMessage> History
        {
            get
            {
                return this.history;
            }
            private set
            {
                if (this.SetPropertyValue("History", ref this.history, value))
                {
                    this.NotifyPropertyChange(MESSAGES_PROPERTY_NAME);
                }
            }
        }

        public ObservableQueue<OutputMessage> RecentHistory
        {
            get
            {
                return this.recentHistory;
            }
            set
            {
                if (this.SetPropertyValue("RecentHistory", ref this.recentHistory, value))
                {
                    this.NotifyPropertyChange(MESSAGES_PROPERTY_NAME);
                }
            }
        }

        public bool ShowRecentMessagesOnly
        {
            get
            {
                return this.showRecentMessagesOnly;
            }
            set
            {
                if (this.SetPropertyValue("ShowRecentMessagesOnly", ref this.showRecentMessagesOnly, value))
                {
                    this.NotifyPropertyChange(MESSAGES_PROPERTY_NAME);
                }
            }
        }

        public ConsoleProjectViewModel Project
        {
            get
            {
                return this.project;
            }
            set
            {
                if (this.SetPropertyValue("Project", ref this.project, value))
                {
                    this.NotifyAvailableCommandsChange();
                }
            }
        }

        public ICommand CloseCommand
        {
            get
            {
                return this.closeCommand.Value;
            }
        }

        public ICommand ProjectCommand
        {
            get
            {
                return this.projectCommand.Value;
            }
        }

        public ICommand RetrieveRecentCommand
        {
            get
            {
                return this.retrieveRecentCommand.Value;
            }
        }

        public ICommand BreakExecutionCommand
        {
            get
            {
                return this.breakExecutionCommand.Value;
            }
        }

        public ICommand ClearHistoryCommand
        {
            get
            {
                return this.clearHistoryCommand.Value;
            }
        }

        public ICommand RunTypedCommandCommand
        {
            get
            {
                return this.runTypedCommandCommand.Value;
            }
        }

        private IList<string> CommandHistory
        {
            get
            {
                return this.commandHistory;
            }
        }

        private int CommandHistoryIndex
        {
            get
            {
                return this.commandHistoryIndex;
            }
            set
            {
                if (value < 0)
                {
                    this.commandHistoryIndex = 0;
                }
                else if (value >= this.CommandHistory.Count)
                {
                    this.commandHistoryIndex = this.CommandHistory.Count - 1;
                }
                else
                {
                    this.commandHistoryIndex = value;
                }
            }
        }

        public SearchViewModel SearchVM
        {
            get
            {
                return this.searchVM;
            }
        }

        public IExplorerViewModel ExplorerVM
        {
            get
            {
                return this.explorerVM;
            }
        }

        public bool AutoScroll
        {
            get
            {
                return this.autoScroll;
            }
            set
            {
                this.SetPropertyValue("AutoScroll", ref this.autoScroll, value);
            }
        }

        public bool AutoSyncWithCurrentFolder
        {
            get
            {
                return this.autoSyncWithCurrentFolder;
            }
            set
            {
                if (value != this.autoSyncWithCurrentFolder)
                {
                    this.autoSyncWithCurrentFolder = value;
                    if (this.ExplorerVM != null)
                    {
                        this.ExplorerVM.AutoCollapseOldItems = value;
                    }
                    this.NotifyPropertyChange("AutoSyncWithCurrentFolder");
                }
            }
        }

        public int MinZoomLevel
        {
            get
            {
                return 10;
            }
        }

        public int MaxZoomLevel
        {
            get
            {
                return 190;
            }
        }

        public int ZoomLevel
        {
            get
            {
                return this.zoomLevel;
            }
            set
            {
                if (value != this.zoomLevel)
                {
                    this.zoomLevel = Math.Min(Math.Max(this.MinZoomLevel, value), this.MaxZoomLevel);
                    this.NotifyPropertyChange("ZoomLevel");
                    this.NotifyPropertyChange("FontSize");
                }
            }
        }

        public double FontSize
        {
            get
            {
                return this.ZoomLevel * DEFAULT_FONT_SIZE / 100;
            }
        }

        public bool IsAlive
        {
            get
            {
                return this.isAlive;
            }
            private set
            {
                if (this.SetPropertyValue("IsAlive", ref this.isAlive, value))
                {
                    this.NotifyWaitIndicatorVisibilityChange();
                }
            }
        }

        public bool IsReady
        {
            get
            {
                return this.isReady;
            }
            set
            {
                if (this.SetPropertyValue("IsReady", ref this.isReady, value))
                {
                    this.NotifyWaitIndicatorVisibilityChange();
                }
            }
        }

        public Visibility WaitIndicatorVisibility
        {
            get
            {
                return (!this.IsAlive || this.IsReady) ? Visibility.Collapsed : Visibility.Visible;
            }
        }

        public bool SuggestionProvided
        {
            get
            {
                return this.suggestionProvided;
            }
            internal set
            {
                this.suggestionProvided = value;
            }
        }

        public bool ShowExplorerPane
        {
            get
            {
                return this.showExplorerPane;
            }
            set
            {
                if (this.SetPropertyValue("ShowExplorerPane", ref this.showExplorerPane, value))
                {
                    this.OnShowExplorerPaneChanged();
                }
            }
        }

        public bool ShowCommandsPane
        {
            get
            {
                return this.showCommandsPane;
            }
            set
            {
                this.SetPropertyValue("ShowCommandsPane", ref this.showCommandsPane, value);
            }
        }

        private void OnShowExplorerPaneChanged()
        {
            if (this.showExplorerPane)
            {
                if (this.AutoSyncWithCurrentFolder)
                {
                    this.ExplorerVM.NavigateTo(this.CurrentFolder);
                }
            }
            else
            {
                this.ExplorerViewWidth = GridLength.Auto;
            }
        }

        public bool IsMultilineCommand
        {
            get
            {
                return this.CurrentCommand != null && this.CurrentCommand.Contains("\n");
            }
        }

        public GridLength ExplorerViewWidth
        {
            get
            {
                return this.explorerViewWidth;
            }
            set
            {
                this.SetPropertyValue("ExplorerViewWidth", ref this.explorerViewWidth, value);
            }
        }

        private IPathSuggestionContext PathSuggestionContext
        {
            get
            {
                return this.pathSuggestionContext;
            }
        }

        public ProcessPerfCounterViewModel CpuUsageVM
        {
            get
            {
                return this.cpuUsageVM;
            }
            private set
            {
                this.SetPropertyValue<ProcessPerfCounterViewModel>("CpuUsageVM", ref this.cpuUsageVM, value);
            }
        }

        public IKeyboardInputHandler KeyboardInputHandler
        {
            get
            {
                return this.keyboardInputHandler;
            }
        }
        #endregion
    }
}